﻿using RockBus.Configuration.Interfaces;
using RockBus.DataContracts;
using RockBus.Logging.Interfaces;
using RockBus.Messages.Interfaces;
using RockBus.PerformanceCounters.Interfaces;
using RockBus.Routing.Interfaces;
using System;
using System.Collections.Generic;
using System.Reflection;
using System.Threading.Tasks;

namespace RockBus.MessageProcessors.SendPorts
{
    public class PersistenceSendPort : IMessageProcessor
    {
        private const string OperationCounterName = "PersistMessage";

        public PersistenceSendPort(
            ILogger logger,
            IReadOnlyConfiguration configuration,
            IPerformanceCounterFactory performanceCounterFactory,
            IMessageFactory messageFactory,
            IQueueManager queueManager,
            ITransportManager transportManager
            )
        {
            this.Logger = logger;
            this.Configuration = configuration;
            this.PerformanceCounterFactory = performanceCounterFactory;
            this.MessageFactory = messageFactory;
            this.QueueManager = queueManager;
            this.TransportManager = transportManager;
        }

        private ILogger Logger { get; set; }

        private IReadOnlyConfiguration Configuration { get; set; }

        private IPerformanceCounterFactory PerformanceCounterFactory { get; set; }

        private IMessageFactory MessageFactory { get; set; }

        private IQueueManager QueueManager { get; set; }

        private ITransportManager TransportManager { get; set; }

        private EndpointInfo GetDestinationEndpoint(IMessageContext messageContext)
        {
            switch (messageContext.ProcessingData.MessageDirection)
            {
                case MessageDirectionEnum.InboundRequest:
                    return this.Configuration.InternalConfiguration.RequestPersistenceEndpoint;

                case MessageDirectionEnum.OutboundRequest:
                    break;

                case MessageDirectionEnum.InboundResponse:
                    return this.Configuration.InternalConfiguration.ResponsePersistenceEndpoint;

                case MessageDirectionEnum.OutboundResponse:
                    break;

                default:
                    break;
            }

            throw new ArgumentException("Invalid MessageDirection.");
        }

        public MessageProcessorEnum ProcessingStep { get { return MessageProcessorEnum.PersistenceSendPort; } }

        public void ProcessMessage(IMessageContext messageContext)
        {
            this.Logger.LogDebug(MethodBase.GetCurrentMethod(), "{0}", messageContext);

            if (messageContext.Exception != null)
            {
                // This persistenceSendPort must Abort the message, because retries are handled by for instance MSMQ
                messageContext.Abort();
                return;
            }
            else if (messageContext.ProcessingData.MessageDirection == MessageDirectionEnum.End)
            {
                // No action needed
                return;
            }

            var oc = this.PerformanceCounterFactory.CreateOperationCounter(OperationCounterName);
            oc.Start();

            try
            {
                var endpoint = this.GetDestinationEndpoint(messageContext);

                var transport = this.TransportManager.GetOrCreateClient(endpoint);

                var taskList = new List<Task>();
                using (var ts = messageContext.CreateTransactionScope(endpoint.TransactionScopeOption, this.Configuration.InternalConfiguration.IsolationLevel, this.Configuration.InternalConfiguration.TransactionTimeoutMs))
                {
                    foreach (var rh in messageContext.RoutingHeaders)
                    {
                        var message = this.MessageFactory.CreatePersistenceMessage(
                            messageContext,
                            rh,
                            transport.MessageVersion);
                        this.Logger.LogDebug(MethodBase.GetCurrentMethod(), "SENDING to {0}: {1}", endpoint.Address, messageContext);
                        var transportTask = transport.StartTransportMessage(message);
                        taskList.Add(transportTask);
                    }

                    // We only need the TransactionScope to be alive when starting the WCF call
                    //  When the call has been made and is executing asynchronously,
                    //   any possible transaction headers are already present in the message's SOAP headers.
                    //  Next to that, the TransactionScope comes off of the messageContext,
                    //   which uses the incoming publisher's Transaction to create the TransactionScope.
                    ts.Complete();
                }

                var taskArray = taskList.ToArray();

                // Success or Failure
                Task.Factory.ContinueWhenAll(taskArray, (tasks) =>
                {
                    try
                    {
                        // Will throw when any task has failed
                        Task.WaitAll(tasks);

                        this.Logger.LogDebug(MethodBase.GetCurrentMethod(), "SENDING SUCCEEDED to {0}: {1}", endpoint.Address, messageContext);
                        messageContext.ProcessingData.MessageDirection = MessageDirectionEnum.End;
                        this.QueueManager.ProcessMessage(messageContext);
                    }
                    catch (Exception exc)
                    {
                        this.Logger.LogError(MethodBase.GetCurrentMethod(), "SENDING FAILED to {0}: {1}; EXCEPTION: ", endpoint.Address, messageContext, exc);

                        messageContext.Exception = exc;
                        this.QueueManager.ProcessException(messageContext);
                    }
                    finally
                    {
                        oc.Complete();
                    }
                });
            }
            catch
            {
                oc.Abort();
                throw;
            }
            //finally
            //{
            //    oc.Complete();
            //}
        }
    }
}